﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Timers;
using JmShared;
using Timer = System.Timers.Timer;
using MySql.Data.MySqlClient;
using System.Data.SqlClient;
using System.Data;

using System.Reflection;
using System.Security.Cryptography;
using WebSocketServer.models;
using WebSocketServer.Utilities;
using System.Threading.Tasks;
using WebSocketServer.Models;
using WebSocketServer.Managers;

namespace WebSocketServer
{

    public class MessageQueueException : Exception
    {
        public MessageQueueException(String message)
            : base(message)
        {
        }
    }
    public class ChatClient
    {
        private static readonly log4net.ILog log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
        private readonly object _lockObject = new object();
        private int _queuedBatchNumber;
        private volatile bool _isBusyWorkingQueue;
        private List<string> _missingEmoticons;
        private AutoResetEvent _waitForEmoticonCheck = new AutoResetEvent(false);

        public AutoResetEvent WaitForEmoticonEvent = new AutoResetEvent(false);
        public AutoResetEvent WaitForAvatarEvent = new AutoResetEvent(false);
        public ConcurrentDictionary<string, bool> EmoticonDownloadQueue { get; set; }
        public JiveClientSession CurrentSession { get; set; }
        /// <summary>
        /// Collection of Usernames in the friends list
        /// </summary>
        public HashSet<Friend> FriendsList { get; set; }
        public UserInfo User { get; set; }
        public UserSettings UserSettings { get; private set; }
        public List<NotificationBase> Notifications { get; set; }
        public string PushUri { get; set; }
        public int DeliveredMessageCount { get; set; }
        public int ReceivedMessageCount { get; set; }
        public Dictionary<string, UnreadUserMessages> UnreadMessageStats { get; set; }
        public DeviceType DeviceType { get; private set; }
        public bool? IsActive { get; set; }

        /// <summary>
        /// Last time the client received a group message, in DateTime.UtcNow()
        /// </summary>
        public DateTime LastGroupMessageReceivedTime { get; set; }
        public DateTime? LastMessageSyncTime { get; set; }
        public int MemberID { get; private set; }
        public int ClientVersion { get; set; }
        public bool IsTilePushActive
        {
            get
            {
                switch (this.DeviceType)
                {
                    default:
                    case DeviceType.Android:
                    case DeviceType.PC:
                        return false;
                    case DeviceType.WindowsPhone:
                        return ((WpUserSettings)this.UserSettings).IsTileEnabled.GetValueOrDefault();
                }
            }
        }

        public double? TimeZoneOffset { get; set; }
        public bool IsToastPushActive
        {
            get
            {
                switch (this.DeviceType)
                {
                    default:
                    case DeviceType.Android:
                        return ((AndroidUserSettings)this.UserSettings).IsToastEnabled.GetValueOrDefault();
                    case DeviceType.PC:
                        return false;
                    case DeviceType.WindowsPhone:
                        return ((WpUserSettings)this.UserSettings).IsToastEnabled.GetValueOrDefault();
                }
            }
        }

        public int OfflineMessageCountLimit
        {
            get
            {
                return this.UserSettings.OfflineMessagesCountLimit.GetValueOrDefault(0);
            }
        }
        public int OfflineMessageAgeLimit
        {
            get
            {
                return this.UserSettings.OfflineMessagesAgeLimit.GetValueOrDefault(0);
            }
        }
        public bool StoreMessagesWhileOffline
        {
            get
            {
                return this.UserSettings.StoreMessagesWhileOffline.GetValueOrDefault(false);
            }
        }

        public bool SendPmPushWhileOffline
        {
            get
            {
                switch (this.DeviceType)
                {
                    default:
                    case DeviceType.PC:
                        return false;
                    case DeviceType.WindowsPhone:
                        return ((WpUserSettings)this.UserSettings).SendPmPushWhileOffline ?? false;
                    case DeviceType.Android:
                        return ((AndroidUserSettings)this.UserSettings).SendPmPushWhileOffline ?? false;
                }
            }
        }
        public string DeviceID
        {
            get
            {
                return this.User != null ? this.User.ID : null;
            }
        }
        public string Username
        {
            get
            {
                return this.User != null ? this.User.UserName : null;
            }
        }
        /// <summary>
        /// Returns the clients UserID in the form Username#DeviceID, e.g user@example.com#13g45gfdg
        /// </summary>
        public string UserID
        {
            get
            {
                return Username + "#" + DeviceID;
            }
        }

        public ChatClient(UserInfo user, JiveClientSession session, DeviceType type, int memberID)
        {
            this.MemberID = memberID;
            this.CurrentSession = session;
            this.User = user;
            this.DeviceType = type;
            UnreadMessageStats = new Dictionary<string, UnreadUserMessages>();
            switch (this.DeviceType)
            {
                case DeviceType.PC:
                    UserSettings = new PcUserSettings(true);
                    break;
                case DeviceType.WindowsPhone:
                    UserSettings = new WpUserSettings(true);
                    PushUri = DB.DbManager.ReadWindowsPushUri(user.ID, memberID);
                    break;
                case DeviceType.Android:
                    UserSettings = new AndroidUserSettings(true);
                    break;
            }

            ReadUserSettings();
            if (this.UserSettings.ManualClientStatus.HasValue && this.UserSettings.ManualClientStatus.Value != ClientStatus.Available)
                this.User.ClientStatus = this.UserSettings.ManualClientStatus.Value;

            this.User.ChatGroups = ReadChatGroups();
            this.Notifications = ReadNotifications();
            this.FriendsList = new HashSet<Friend>(DB.DbManager.ReadFriendsList(this.MemberID));

            var userSettingProperties = this.UserSettings.GetType().GetProperties().Where(prop => prop.CanRead && prop.CanWrite);
            foreach (var prop in userSettingProperties)
            {
                var value = prop.GetValue(this.UserSettings, null);
                if (value != null)
                {
                    var attributes = prop.GetCustomAttributes(typeof(UserInfoAttribute), false);
                    TryUpdateUserInfo(attributes, value, this.User);
                }
            }
        }
        /// <summary>
        /// Returns the client status for the user, overridden by the manual status setting if set.
        /// </summary>
        /// <param name="username"></param>
        /// <returns></returns>
        public ClientStatus? GetOverriddenClientStatusForUser(string username)
        {
            ClientStatus? status = this.User.ClientStatus;
            bool isIgnored = this.UserSettings.IgnoredContacts != null && this.UserSettings.IgnoredContacts.Contains(username, StringComparer.OrdinalIgnoreCase);
            if (isIgnored)
                status = ClientStatus.Offline;

            return status;
        }

        public void SendStoredGroupMessages(MessageRequest request)
        {
            List<ChatMessage> messages = DB.DbManager.ReadGroupMessageQueue(request.ID, this.MemberID, request.CountLimit, request.AgeLimitHours);
            SendStoredMessages(messages, request.ID);
        }
        public void SendStoredPrivateMessages(MessageRequest request)
        {
            List<ChatMessage> messages = DB.DbManager.ReadPrivateMessageQueue(request.ID, UserID, MemberID, request.CountLimit, request.AgeLimitHours);
            SendStoredMessages(messages, request.ID);
        }

        private void SendStoredMessages(List<ChatMessage> messages, string chatID)
        {
            if (messages.Count == 0)
                return;
            if (DeviceType == JmShared.DeviceType.PC && ClientVersion < 1151)
            {
                SendStoredMessagesOldWay(messages, chatID);
            }
            else
            {
                MessageQueue queue = new MessageQueue()
                {
                    ChatID = chatID,
                    MessageCount = messages.Count,
                    Messages = messages
                };
                CurrentSession.SendMessageQueue(queue);

            }
        }
        [Obsolete("Used for backwards compability. Functionality replaced with MessageQueue class.")]
        private void SendStoredMessagesOldWay(List<ChatMessage> messages, string chatID)
        {
            try
            {
                _isBusyWorkingQueue = true;
                lock (this._lockObject)
                {
                    int batchNum = Interlocked.Increment(ref this._queuedBatchNumber);
                    int messangeNum = 1;
                    int lastSentBatchNum = 0;
                    foreach (ChatMessage msg in messages)
                    {
                        int count = messages.Count;

                        msg.MessageQueueInfo = new MessageQueueInfo(messangeNum, count, batchNum);
                        messangeNum++;

                        if (!CurrentSession.SendMessage(msg))
                            throw new OperationCanceledException("Sending of ChatMessage failed");

                        Debug.Print("Sendt message and removed message from queue: {0} on thread: {1}", msg.Message, Thread.CurrentThread.ManagedThreadId);
                        lastSentBatchNum = batchNum;

                        // Set is never called, so we wait always until the timeout occurs
                        using (EventWaitHandle tmpEvent = new ManualResetEvent(false))
                        {
                            tmpEvent.WaitOne(TimeSpan.FromMilliseconds(3));
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                log.Error("Exception occured while processing message queue.", ex);
            }
            finally
            {
                _isBusyWorkingQueue = false;
            }
        }
        /// <summary>
        /// Processes the stored message queue for the user, sends the relevant messages and sets the last sync time flag.
        /// </summary>
        public void ProcessQueue()
        {
            if (DeviceType == JmShared.DeviceType.PC && ClientVersion < 1151)
            {
                ProcessQueueOldWay();
            }
            else
            {
                DateTime? lastSendtMessageTime = null;

                try
                {
                    lock (this._lockObject)
                    {
                        List<ChatMessage> storedMessages = DB.DbManager.ReadQueuedMessages(this.User, this.MemberID, this.DeviceID, this.OfflineMessageCountLimit, this.OfflineMessageAgeLimit);

                        if (storedMessages.Count == 0)
                            return;

                        List<List<ChatMessage>> differentRecipients =
                              storedMessages.Select((x) => x)
                               .GroupBy(x => x.RecipientID)
                               .Select(x => x.Select(v => v).Cast<ChatMessage>().ToList())
                               .ToList();

                        foreach (List<ChatMessage> msgList in differentRecipients)
                        {
                            MessageQueue queue = new MessageQueue()
                            {
                                ChatID = msgList.First().RecipientID,
                                MessageCount = msgList.Count,
                                Messages = msgList
                            };
                            if (CurrentSession.SendMessageQueue(queue))
                            {
                                var newLastSendtMsgTime = msgList.Last().MessageRelayTime.Value;
                                if (newLastSendtMsgTime > lastSendtMessageTime.GetValueOrDefault())
                                    lastSendtMessageTime = newLastSendtMsgTime;
                            }
                        }
                    }
                }
                catch (Exception err)
                {
                    log.Error("Exception occured while processing message queue.", err);
                }
                finally
                {
                    //if (lastSendtMessageTime.HasValue)
                    UpdateDeviceSync(lastSendtMessageTime);
                    if (DeviceType == DeviceType.Android || DeviceType == DeviceType.WindowsPhone)
                        ResetDeliveredMessageCount();

                    //else
                    //    UpdateDeviceSync();
                }
            }
        }

        /// <summary>
        /// Processes the stored message queue for the user, sends the relevant messages and sets the last sync time flag.
        /// </summary>
        [Obsolete("Used for backwards compability. Functionality replaced with MessageQueue class.")]
        public void ProcessQueueOldWay()
        {
            DateTime? lastSendtMessageTime = null;
            try
            {
                _isBusyWorkingQueue = true;
                lock (this._lockObject)
                {
                    List<ChatMessage> storedMessages = DB.DbManager.ReadQueuedMessages(this.User, this.MemberID, this.DeviceID, this.OfflineMessageCountLimit, this.OfflineMessageAgeLimit);

                    if (storedMessages.Count == 0)
                        return;


                    var QueueWithBatchNum = new List<KeyValuePair<int, ChatMessage>>();

                    List<List<ChatMessage>> differentRecipients =
                          storedMessages.Select((x) => x)
                           .GroupBy(x => x.RecipientID)
                           .Select(x => x.Select(v => v).Cast<ChatMessage>().ToList())
                           .ToList();

                    foreach (List<ChatMessage> msgList in differentRecipients)
                    {
                        int batchNum = Interlocked.Increment(ref this._queuedBatchNumber);
                        foreach (ChatMessage msg in msgList)
                            QueueWithBatchNum.Add(new KeyValuePair<int, ChatMessage>(batchNum, msg));
                    }

                    int messangeNum = 1;
                    int lastSentBatchNum = 0;
                    foreach (KeyValuePair<int, ChatMessage> kvp in QueueWithBatchNum)
                    {
                        if (CurrentSession == null)
                            return;

                        int batchNum = kvp.Key;
                        if (batchNum != lastSentBatchNum)
                            messangeNum = 1;
                        ChatMessage msg = kvp.Value;
                        int count = QueueWithBatchNum.Count(x => x.Key == batchNum);

                        msg.MessageQueueInfo = new MessageQueueInfo(messangeNum, count, batchNum);
                        messangeNum++;

                        if (CurrentSession.SendMessage(msg))
                            lastSendtMessageTime = msg.MessageRelayTime;
                        else
                            throw new OperationCanceledException("Sending of ChatMessage failed");

                        Debug.Print("Sendt message and removed message from queue: {0} on thread: {1}", msg.Message, Thread.CurrentThread.ManagedThreadId);
                        lastSentBatchNum = batchNum;

                        // Set is never called, so we wait always until the timeout occurs
                        using (EventWaitHandle tmpEvent = new ManualResetEvent(false))
                        {
                            tmpEvent.WaitOne(TimeSpan.FromMilliseconds(3));
                        }
                    }
                }
            }
            catch (Exception err)
            {
                log.Error("Exception occured while processing message queue.", err);
            }
            finally
            {
                _isBusyWorkingQueue = false;
                if (lastSendtMessageTime.HasValue)
                    UpdateDeviceSync(lastSendtMessageTime.Value);
                else
                    UpdateDeviceSync();
            }
        }
        /// <summary>
        /// Updates the user device's connection, authentication status and synced message time. If lastSendtMessageTime, isOffline or isLoggedIn is null, the values are not updated.
        /// </summary>
        public void UpdateDeviceSync(DateTime? lastMessageSynctime = null, bool? isConnected = null, bool? isLoggedIn = null)
        {
            if (lastMessageSynctime == null)
                lastMessageSynctime = this.LastMessageSyncTime;
            DB.DbManager.StoreDeviceSync(this.User.ID, this.DeviceType, this.MemberID, isLoggedIn, lastMessageSynctime, isConnected, TimeZoneOffset);
        }
        /// <summary>
        /// Resets the received and delivered message count for this device
        /// </summary>
        public void ResetDeliveredMessageCount()
        {
            DeliveredMessageCount = ReceivedMessageCount = 0;
            DB.DbManager.ResetDeliveredMessageCount(User.ID, this.MemberID);
        }
        /// <summary>
        /// Write this clients delivered and undelivered message count to the database
        /// </summary>
        public void StoreDeliveredMessageCount()
        {
            DB.DbManager.StoreDeliveredMessageCount(this);
        }

        /// <summary>
        /// Sends the current list of notifications to the connected client.
        /// </summary>
        /// <param name="refreshFromDatabase"></param>
        public void UpdateNotifications(bool refreshFromDatabase)
        {
            if (refreshFromDatabase)
                this.Notifications = ReadNotifications();

            try
            {
                var notificationList = new NotificationList()
                {
                    FriendInviteNotifications = Notifications.Where(x => x is FriendInviteNotification).Cast<FriendInviteNotification>(),
                    GroupInviteNotifications = Notifications.Where(x => x is GroupInviteNotification).Cast<GroupInviteNotification>(),
                    ServerMessageNotifications = Notifications.Where(x => x is ServerMessageNotification).Cast<ServerMessageNotification>(),
                    UpdateAvailableNotifications = Notifications.Where(x => x is UpdateAvailableNotification).Cast<UpdateAvailableNotification>()
                };

                CurrentSession.SendJsonMessage(ClientCommands.NotificationsUpdate.ToString(), notificationList);
            }
            catch (Exception ex)
            {
                log.Error("Error in SendNotifications: ", ex);
            }
        }

        /// <summary>
        /// Sends the current UserInfo to the connected client
        /// </summary>
        public void UpdateUserInfo()
        {
            ServerInfoMessage infoMsg = new ServerInfoMessage { MessageType = ServerInfoMessageType.UserInfoUpdated, UserInfo = this.User };
            this.CurrentSession.SendServerInfoMessage(infoMsg);
        }

        /// <summary>
        /// Upodates the users chat groups from the database
        /// </summary>
        public void SetUsersChatGroups()
        {
            this.User.ChatGroups = ReadChatGroups();
        }

        /// <summary>
        /// Broadcasts chat groups to online contacs.
        /// </summary>
        public void BroadcastChatGroups(bool refreshFromDatabase, MessageReceipient recipients)
        {
            if (refreshFromDatabase)
                this.User.ChatGroups = ReadChatGroups();

            try
            {
                ServerInfoMessage msg = new ServerInfoMessage { MessageType = ServerInfoMessageType.UserInfoUpdated, UserInfo = this.User };
                if (recipients == MessageReceipient.Contacts)
                    CurrentSession.AppServer.SendToAll(msg, CurrentSession, false, true);
                else if (recipients == MessageReceipient.ContactsAndClient)
                    CurrentSession.AppServer.SendToAll(msg, CurrentSession, true, true);
                else if (recipients == MessageReceipient.Client)
                    CurrentSession.SendServerInfoMessage(msg);
            }
            catch (Exception ex)
            {
                log.Error("Error in BroadcastChatGroups: ", ex);
            }
        }

        /// <summary>
        /// Reads the users chat groups from the database.
        /// </summary>
        /// <returns></returns>
        private List<ChatGroup> ReadChatGroups()
        {
            return DB.DbManager.ReadChatGroups(this.Username);
        }
        /// <summary>
        /// Reads the users notifications from the database.
        /// </summary>
        /// <returns></returns>
        private List<NotificationBase> ReadNotifications()
        {
            return DB.DbManager.ReadNotifications(this.MemberID);
        }

        /// <summary>
        /// Reads the users settings into the UserSettings property.
        /// </summary>
        private void ReadUserSettings()
        {
            DataTable table = DB.DbManager.ReadUserSettings(this.DeviceType, this.MemberID, this.User.ID);
            if (table.Rows[0][table.Columns["deviceid"]] == DBNull.Value)
                table = DB.DbManager.ReadUserSettings(this.DeviceType, this.MemberID, this.User.ID, true);
            var properties = this.UserSettings.GetType().GetProperties().Where(prop => prop.CanRead && prop.CanWrite);
            this._missingEmoticons = new List<string>();
            foreach (DataRow row in table.Rows)
            {
                foreach (DataColumn col in table.Columns)
                {
                    foreach (PropertyInfo pi in properties)
                    {
                        if (col.ColumnName == pi.Name.ToLower() && (row[col] != DBNull.Value))
                        {

                            if (col.ColumnName == "customemoticons")
                            {
                                var customEmoticons = new HashSet<ContactEmoticon>();
                                var emoticons = ((string)row[col]).Split(',');
                                foreach (string str in emoticons)
                                {
                                    var emoticon = str.Split('|');
                                    if (emoticon.Length == 5)
                                    {
                                        customEmoticons.Add(new ContactEmoticon()
                                        {
                                            Keyword = emoticon[0],
                                            Hash = emoticon[2],
                                            Index = Convert.ToInt32(emoticon[3]),
                                            Extension = emoticon[4]
                                        });

                                        if (emoticon[1] == "0")
                                            _missingEmoticons.Add(emoticon[0]);
                                    }
                                }

                                pi.SetValue(this.UserSettings, customEmoticons, null);
                            }
                            else if (col.ColumnName == "openchats")
                            {
                                List<OpenChat> chats = new List<OpenChat>();
                                var chatTypes = ((string)row[col]).Split(':');
                                foreach (string type in chatTypes)
                                {
                                    var openChats = type.Split(',');
                                    foreach (string str in openChats)
                                    {
                                        var openChat = str.Split('|');
                                        if (openChat.Length == 2)
                                        {
                                            bool isPinned = false;
                                            int pinnedInt = 0;
                                            if (Int32.TryParse(openChat[1], out pinnedInt))
                                                isPinned = Convert.ToBoolean(pinnedInt);
                                            int groupID;
                                            if (Int32.TryParse(openChat[0], out groupID))
                                            {
                                                chats.Add(new OpenChat(groupID.ToString(), isPinned, ChatType.GroupChat));
                                            }
                                            else
                                            {
                                                chats.Add(new OpenChat(openChat[0], isPinned, ChatType.PrivateMessage));
                                            }
                                        }
                                    }
                                }
                                pi.SetValue(this.UserSettings, chats, null);
                            }
                            else if (col.ColumnName == "avatar")
                            {
                                string[] data = ((string)row[col]).Split('|');
                                if (data.Length == 2)
                                {
                                    Avatar avatar = new Avatar { Hash = data[0], Extension = data[1] };
                                    pi.SetValue(this.UserSettings, avatar, null);
                                }
                            }
                            else if (col.ColumnName == "expandedgroups")
                                pi.SetValue(this.UserSettings, ((string)row[col]).Split(',').ToList(), null);
                            else if ((col.ColumnName == "opengroupchats" || col.ColumnName == "ignoredcontacts" || col.ColumnName == "blockedgroups"))
                                pi.SetValue(this.UserSettings, ((string)row[col]).Split(',').ToList(), null);
                            else if (col.ColumnName == "manualclientstatus")
                                pi.SetValue(this.UserSettings, Enum.Parse(typeof(ClientStatus), row[col].ToString()), null);
                            else
                                pi.SetValue(this.UserSettings, row[col], null);
                        }
                    }
                }
            }
        }
        internal void RequestMissingEmoticons()
        {
            if (_missingEmoticons != null && _missingEmoticons.Count > 0)
            {
                RequestMissingEmoticons(this._missingEmoticons);

            }
        }

        internal void RequestMissingEmoticons(List<string> keys)
        {
            EmoticonDownloadQueue = new ConcurrentDictionary<string, bool>();
            Task.Factory.StartNew((obj) =>
                {
                    var keywords = (List<string>)obj;
                    foreach (string keyword in keywords)
                    {
                        if (CurrentSession == null)
                            break;
                        EmoticonDownloadQueue.GetOrAdd(keyword, false);
                        EmoticonRequest emoticonReq = new EmoticonRequest(keyword, this.DeviceID, this.Username);
                        this.CurrentSession.SendEmoticonRequest(emoticonReq);
                    }
                    _missingEmoticons.Clear();
                }, keys);
        }

        internal void RequestMissingAvatar(Avatar avatar)
        {
            //Task.Factory.StartNew(() =>
            //{
            if (CurrentSession == null)
                return;
            AvatarRequest avatarReq = new AvatarRequest(avatar.Hash, avatar.Extension, this.DeviceID, this.Username);
            this.CurrentSession.SendAvatarRequest(avatarReq);
            //});
        }
        /// <summary>
        /// Creates the ClientList for this user. The ClientList contains the users contacts and the users chat groups.
        /// </summary>
        /// <returns></returns>
        internal ClientList CreateClientList()
        {
            List<UserInfo> clients = new List<UserInfo>();
            foreach (Friend friend in FriendsList)
            {
                ConcurrentDictionary<string, ChatClient> friendDevices;
                if (Clients.ConnectedClientsByNameAndDeviceID.TryGetValue(friend.Username, out friendDevices))
                {
                    foreach (KeyValuePair<string, ChatClient> kvp in friendDevices)
                    {
                        if (!clients.Any(x => x.UserID == kvp.Value.UserID))
                        {
                            UserInfo user = kvp.Value.User.Clone();
                            ClientStatus? overriddenStatus = kvp.Value.GetOverriddenClientStatusForUser(Username);
                            if (overriddenStatus.HasValue)
                                user.ClientStatus = overriddenStatus.Value;
                            clients.Add(user);
                        }
                    }
                }
            }
            ConcurrentDictionary<string, ChatClient> ourClients = new ConcurrentDictionary<string, ChatClient>();
            if (Clients.ConnectedClientsByNameAndDeviceID.TryGetValue(Username, out ourClients))
            {
                foreach (KeyValuePair<string, ChatClient> kvp in ourClients)
                {
                    if (kvp.Key == DeviceID)
                        continue;
                    clients.Add(kvp.Value.User);
                }
            }
            var contactClients = DB.DbManager.ReadContactClients(MemberID, DeviceID);
            foreach (UserInfo user in contactClients)
            {
                if (!clients.Any(x => x.UserID == user.UserID))
                {
                    //This is the first client from this user, so we need to add account specific groups, avatar and custom emoticons,
                    //since there are no online clients.
                    if (!clients.Any(x => x.UserName == user.UserName))
                    {
                        user.Avatar = DB.DbManager.ReadAvatar(user.UserName);
                        user.CustomEmoticons = DB.DbManager.ReadCustomEmoticons(user.UserName);
                        user.ChatGroups = DB.DbManager.ReadChatGroups(user.UserName);
                    }
                    clients.Add(user);
                }
            }
            return new ClientList(clients, FriendsList.Where(x => !x.IsGroupContact).Select(x => x.Username).ToList(), User.ChatGroups);
        }
        /// <summary>
        /// Updates the stored UserSettings for the client, and broadcasts the changes to the users contacts.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="source"></param>
        public void UpdateSettings<T>(T source)
        {
            try
            {
                Type t = typeof(T);
                bool sendUserInfoUpdate = false;
                var settings = this.UserSettings;

                UserInfo ui = new UserInfo(this.User.UserName, this.User.ID) { };
                var userSettingProperties = t.GetProperties().Where(prop => prop.CanRead && prop.CanWrite);
                Dictionary<PropertyInfo, object> updatedValues = new Dictionary<PropertyInfo, object>();
                foreach (var prop in userSettingProperties)
                {
                    var value = prop.GetValue(source, null);
                    var attributes = prop.GetCustomAttributes(typeof(UserInfoAttribute), false);
                    if (value != null)// || (attributes.Length > 0 && ((UserInfoAttribute)attributes[0]).PropertyType == UserInfoAttributeType.Avatar))
                    {
                        prop.SetValue(settings, value, null);
                        updatedValues.Add(prop, value);

                        if (TryUpdateUserInfo(attributes, value, this.User, ui))
                        {
                            sendUserInfoUpdate = true;
                        }
                    }
                }
                DB.DbManager.StoreUserSettings(updatedValues, this.DeviceType, this.MemberID, this.User.ID);
                if (sendUserInfoUpdate)
                {
                    Task.Factory.StartNew(() =>
                        {
                            CheckForMissingEmoticons(ui);
                            CheckForMissingAvatar(ui);
                            _waitForEmoticonCheck.WaitOne(TimeSpan.FromSeconds(30));
                            if (ui.ChatGroups == null)
                                ui.ChatGroups = this.User.ChatGroups;
                            ServerInfoMessage msg = new ServerInfoMessage
                            {
                                MessageType = ServerInfoMessageType.UserInfoUpdated,
                                UserInfo = ui
                            };
                            if (this.CurrentSession != null)
                                this.CurrentSession.AppServer.SendToAll(msg, this.CurrentSession, true, true);//!this.CurrentSession.PrepareLogout, true);
                        });
                }
            }
            catch (Exception ex)
            {
                log.Error("Error in UpdateSettings: ", ex);
            }
        }

        private void CheckForMissingAvatar(UserInfo ui)
        {
            if (ui.Avatar != null && !string.IsNullOrEmpty(ui.Avatar.Hash))
            {
                bool exists = DB.DbManager.CheckIfAvatarExists(ui.Avatar.Hash);
                if (!exists)
                {
                    log.Debug(string.Format("We have a  missing avatar for user {0}", this.Username));

                    this.User.Avatar = ui.Avatar;
                    this.UserSettings.Avatar = ui.Avatar;
                    RequestMissingAvatar(ui.Avatar);
                }
            }
        }

        /// <summary>
        /// Checks the database for missing emoticons, and sends a download request to the client.
        /// </summary>
        /// <param name="ui"></param>
        private void CheckForMissingEmoticons(UserInfo ui)
        {

            bool updatedEmoticons = false;
            if (ui.CustomEmoticons != null && ui.CustomEmoticons.Count > 0)
            {
                var missingEmoticonsForUser = DB.DbManager.ReadMissingEmoticonKeywordsForUser(this.MemberID);
                if (missingEmoticonsForUser.Count > 0)
                {
                    log.Debug(string.Format("We have {0} missing emoticons for user {1}", missingEmoticonsForUser.Count, this.Username));
                    updatedEmoticons = true;
                    Task.Factory.StartNew(() =>
                    {
                        log.Debug(string.Format("Waiting for received emoticon event for {0} at {1}", ui.UserName, DateTime.Now.ToLongTimeString()));
                        WaitForEmoticonEvent.WaitOne(TimeSpan.FromSeconds(30));
                        foreach (KeyValuePair<string, bool> kvp in EmoticonDownloadQueue)
                        {
                            if (kvp.Value == false)
                            {
                                ContactEmoticon ce = ui.CustomEmoticons.FirstOrDefault(x => x.Keyword == kvp.Key);
                                if (ce != null)
                                {
                                    if (ui.CustomEmoticons.Remove(ce))
                                    {
                                        //If we still have missing emoticons after 30 seconds, we delete the emoticons to avoid NULL values.
                                        log.Debug(string.Format("Removed emoticon {0} from member {1}", kvp.Key, this.Username));
                                    }
                                    else
                                        log.Warn(string.Format("Failed to remove emoticon {0} from member {1}", kvp.Key, this.Username));
                                }
                            }
                        }

                        this.User.CustomEmoticons = ui.CustomEmoticons;
                        this.UserSettings.CustomEmoticons = ui.CustomEmoticons;
                        _waitForEmoticonCheck.Set();
                    });
                    RequestMissingEmoticons(missingEmoticonsForUser);
                }
            }
            if (!updatedEmoticons)
                _waitForEmoticonCheck.Set();
        }

        /// <summary>
        /// Try to update user info objects from attribute. Returns updated property type, or null if none are updated.
        /// </summary>
        /// <param name="t"></param>
        /// <returns></returns>
        private bool TryUpdateUserInfo(object[] attributes, object value, UserInfo localUser, UserInfo userToBroadcast = null)
        {
            bool sendUserInfoUpdate = false;
            if (attributes.Length > 0)
            {
                UserInfoAttribute att = attributes[0] as UserInfoAttribute;
                if (att != null)
                {
                    switch (att.PropertyType)
                    {
                        case UserInfoAttributeType.FontFamily:
                            if ((string)value != localUser.FontFamily)
                            {
                                localUser.FontFamily = (string)value;
                                if (userToBroadcast != null)
                                    userToBroadcast.FontFamily = (string)value;
                                sendUserInfoUpdate = true;
                            }
                            break;
                        case UserInfoAttributeType.FontColor:
                            if ((string)value != localUser.Color)
                            {
                                localUser.Color = (string)value;
                                if (userToBroadcast != null)
                                    userToBroadcast.Color = (string)value;
                                sendUserInfoUpdate = true;
                            }
                            break;
                        case UserInfoAttributeType.FontStyle:
                            if ((string)value != localUser.FontStyle)
                            {
                                localUser.FontStyle = (string)value;
                                if (userToBroadcast != null)
                                    userToBroadcast.FontStyle = (string)value;
                                sendUserInfoUpdate = true;
                            }
                            break;
                        case UserInfoAttributeType.FontWeight:
                            if ((string)value != localUser.FontWeight)
                            {
                                localUser.FontWeight = (string)value;
                                if (userToBroadcast != null)
                                    userToBroadcast.FontWeight = (string)value;
                                sendUserInfoUpdate = true;
                            }
                            break;
                        case UserInfoAttributeType.Nickname:
                            if ((string)value != localUser.NickName)
                            {
                                localUser.NickName = (string)value;
                                if (userToBroadcast != null)
                                    userToBroadcast.NickName = (string)value;
                                sendUserInfoUpdate = true;
                            }
                            break;
                        case UserInfoAttributeType.Avatar:
                            if (!((Avatar)value).Equals(localUser.Avatar))
                            {
                                localUser.Avatar = (Avatar)value;
                                if (userToBroadcast != null)
                                    userToBroadcast.Avatar = (Avatar)value;
                                sendUserInfoUpdate = true;
                            }
                            break;
                        case UserInfoAttributeType.ChatGroups:
                            if (localUser.ChatGroups == null || (value == null || !((List<ChatGroup>)value).SequenceEqual(localUser.ChatGroups)))
                            {
                                localUser.ChatGroups = (List<ChatGroup>)value;
                                if (userToBroadcast != null)
                                    userToBroadcast.ChatGroups = value != null ? (List<ChatGroup>)value : new List<ChatGroup>();
                                sendUserInfoUpdate = true;
                            }
                            break;
                        case UserInfoAttributeType.CustomEmoticons:
                            if (localUser.CustomEmoticons == null || (value == null || !((HashSet<ContactEmoticon>)value).SequenceEqual(localUser.CustomEmoticons)))
                            {
                                localUser.CustomEmoticons = (HashSet<ContactEmoticon>)value;
                                if (userToBroadcast != null)
                                    userToBroadcast.CustomEmoticons = value != null ? (HashSet<ContactEmoticon>)value : new HashSet<ContactEmoticon>();
                                sendUserInfoUpdate = true;
                            }
                            break;
                    }
                }
            }
            return sendUserInfoUpdate;
        }
    }
}
